package control;

import model.Direction;
import model.interfaces.commandTypes.DirectionArgs;
import model.interfaces.commandTypes.MoveArgs;
import model.interfaces.commandTypes.NoArgs;
import model.interfaces.commandTypes.ArmyArgs;
import model.interfaces.selectable.ControllableModel;
import model.interfaces.selectable.ControllablePlayer;
import model.interfaces.selectable.InstanceTypeAndHealth;
import model.InstanceTypeAndHealthClass;
import model.interfaces.selectable.SelectableGroup;
import model.interfaces.selectable.SelectableRallyPoint;
import model.interfaces.selectable.SelectableType;
import model.interfaces.selectable.SelectableInstance;
import model.interfaces.selectable.SelectableCommand;

import java.awt.Point;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import view.View;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Manages the hotkeys for the View's HUD. Responsible for telling the View what
 * to display in its HUD, and for making instances perform commands.
 * 
 * @author Ross Nichols
 */

class MainAreaKeyAdapter extends GameKeyAdapter
{
    private SelectableGroup group = null;
    private SelectableType type = null;
    private SelectableInstance instance = null;
    private SelectableCommand command = null;
    private ArgState argState = null;
    
    final private String NO_GROUP = "<Group>";
    final private String NO_TYPE = "<Type>";
    final private String NO_INSTANCE = "<Instance>";
    final private String NO_COMMAND = "<Command>";
    final private int CLEAR_QUEUE = KeyEvent.VK_C;
    
    private String groupStr = null;
    private String typeStr = null;
    private String instanceStr = null;
    private String commandStr = null;
    
    private View view;
    private ControllablePlayer player;
    
    /**
     * Upon construction, we need to store references to the Player and the
     * View.
     */
    public MainAreaKeyAdapter( ControllableModel m, View v ) {
        view = v;
        player = m.getPlayer();
    }
    
    /**
     * When activated, we clear out all selections.
     */
    protected void doActivation()
    {
        System.out.println( "MA activated" );
        clearSelections();
    }
    
    /**
     * When deactivated, we clear out all selections.
     */
    protected void doDeactivation()
    {
        clearSelections();
        System.out.println( "MA deactivated" );
    }
    
    /**
     * Clears out any selections in the HUD.
     */
    private void clearSelections()
    {
        group = null;
        type = null;
        instance = null;
        command = null;
        argState = null;
        updateView();
        System.out.println( "selections are cleared" );
    }
    
    /**
     * Updates the view with new information.
     */
    private void updateView()
    {
        String newGroup = group == null ? NO_GROUP : group.getName();
        String newType = type == null ? NO_TYPE : type.getName();
        String newInstance = instance == null ? NO_INSTANCE : String.format( "%s (%d)",
                                                                             instance.getName(),
                                                                             instance.getID() );
        String newCommand = command == null ? NO_COMMAND : command.getName();
        
        if ( !newGroup.equals( groupStr ) )
        {
            groupStr = newGroup;
            view.setGroupBox( groupStr );
        }
        if ( !newType.equals( typeStr ) )
        {
            typeStr = newType;
            view.setTypeBox( typeStr );
        }
        if ( !newInstance.equals( instanceStr ) )
        {
            instanceStr = newInstance;
            view.setInstanceBox( instanceStr );
            // the status overview is linked to the instance
            updateStatus();
        }
        if ( !newCommand.equals( commandStr ) )
        {
            commandStr = newCommand;
            view.setCommandBox( commandStr );
        }
    }
    
    /**
     * Updates the View's status overview.
     */
    private void updateStatus()
    {
        InstanceTypeAndHealth[] status;
        if ( instance == null )
            status = new InstanceTypeAndHealthClass[ 0 ];
        else
            status = instance.getStatus();
        
        /*
         * for ( int i = 0; i != status.length; ++i ) { if ( status[ i ] == null
         * ) { System.out.println( "FAIL " + i + " " + status.length ); } }
         */
        view.setStatusOverview( status );
    }
    
    /**
     * Receives the key presses when activated.
     * 
     * @param e
     *            the KeyEvent
     */
    public void keyPressed( KeyEvent e )
    {
        // System.out.println( "MA is getting these" );
        int code = e.getKeyCode();
        
        // ESCAPE clears the current selections
        if ( code == KeyEvent.VK_ESCAPE )
            clearSelections();
        
        // Control+UP/DOWN toggle between groups
        else if ( (code == KeyEvent.VK_UP || code == KeyEvent.VK_DOWN) && e.isControlDown() )
        {
            // group
            List< SelectableGroup > groups = Arrays.asList( player.getGroups() );
            if ( group == null )
            {
                group = code == KeyEvent.VK_UP ? groups.get( 0 ) : groups.get( groups.size() - 1 );
            }
            else
            {
                int offset = code == KeyEvent.VK_UP ? 1 : -1;
                int ind = (groups.indexOf( group ) + offset) % groups.size();
                if ( ind < 0 ) ind += groups.size();
                group = groups.get( ind );
            }
            
            // when the group changes, all other boxes must be reset
            System.out.println( "new group: " + group.getName() );
            type = null;
            instance = null;
            command = null;
            argState = null;
            
            // in the case that the number of types is 1 (Rally Points!),
            // we immediately select it.
            List< SelectableType > types = Arrays.asList( group.getTypes() );
            if ( types.size() == 1 )
            {
                type = types.get( 0 );
                System.out.println( "new type: " + type.getName() );
            }
        }
        
        // Control+LEFT/RIGHT toggle types
        else if ( (code == KeyEvent.VK_LEFT || code == KeyEvent.VK_RIGHT) && e.isControlDown()
                && group != null )
        {
            // type
            List< SelectableType > types = Arrays.asList( group.getTypes() );
            
            // only accept input when there are choices of types
            if ( types.size() > 1 )
            {
                if ( type == null )
                {
                    type = code == KeyEvent.VK_RIGHT ? types.get( 0 )
                            : types.get( types.size() - 1 );
                }
                else
                {
                    int offset = code == KeyEvent.VK_RIGHT ? 1 : -1;
                    int ind = (types.indexOf( type ) + offset) % types.size();
                    if ( ind < 0 ) ind += types.size();
                    type = types.get( ind );
                }
                
                // when the type changes, the boxes to the right must be reset
                System.out.println( "new type: " + type.getName() );
                instance = null;
                command = null;
                argState = null;
            }
        }
        
        // LEFT/RIGHT toggle instances
        else if ( (code == KeyEvent.VK_LEFT || code == KeyEvent.VK_RIGHT) && type != null )
        {
            // instance
            List< SelectableInstance > instances = Arrays.asList( type.getInstances() );
            if ( instance == null && instances.size() > 0 )
            {
                instance = code == KeyEvent.VK_RIGHT ? instances.get( 0 )
                        : instances.get( instances.size() - 1 );
            }
            else if ( instances.size() > 0 )
            {
                int offset = code == KeyEvent.VK_UP ? 1 : -1;
                int ind = (instances.indexOf( instance ) + offset) % instances.size();
                if ( ind < 0 ) ind += instances.size();
                instance = instances.get( ind );
            }
            
            // when the instance changes, the command (and its arg listener)
            // must be reset
            if ( instance != null ) System.out.println( "new instance: " + instance.getName() );
            command = null;
            argState = null;
        }
        
        // selecting an instance by its ID is also an acceptable instance
        // selection mechanism
        else if ( code >= KeyEvent.VK_0 && code <= KeyEvent.VK_9 && type != null
                && !e.isControlDown() && argState != armyArgs )
        {
            // alternative for instances
            List< SelectableInstance > instances = Arrays.asList( type.getInstances() );
            int id = -1;
            if ( code >= KeyEvent.VK_1 && code <= KeyEvent.VK_9 )
            {
                id = code - KeyEvent.VK_0;
            }
            else if ( code == KeyEvent.VK_0 )
            {
                id = 10;
            }
            for ( SelectableInstance inst : instances )
            {
                if ( inst.getID() == id )
                {
                    instance = inst;
                    
                    // when the instance changes, the command (and its arg
                    // listener) must be reset
                    System.out.println( "new instance: " + instance.getName() );
                    command = null;
                    argState = null;
                }
            }
        }
        
        // CLEAR_QUEUE clears the instance's queue
        else if ( code == CLEAR_QUEUE && instance != null )
        {
            instance.cancelPendingOrders();
        }
        
        // UP/DOWN toggle commands
        else if ( (code == KeyEvent.VK_UP || code == KeyEvent.VK_DOWN) && instance != null )
        {
            // command
            List< SelectableCommand > commands = Arrays.asList( instance.getCommands() );
            if ( command == null )
            {
                command = code == KeyEvent.VK_UP ? commands.get( 0 )
                        : commands.get( commands.size() - 1 );
            }
            else
            {
                int offset = code == KeyEvent.VK_UP ? 1 : -1;
                int ind = (commands.indexOf( command ) + offset) % commands.size();
                if ( ind < 0 ) ind += commands.size();
                command = commands.get( ind );
                view.setSelectedPath( new Point[ 0 ] );
            }
            
            // when the command is changes, its argstate must be reset
            System.out.println( "new command: " + command.getName() );
            if ( command == null )
                argState = null;
            else
                command.accept( this );
        }
        
        // numpad 5 centers the view on the current instance, unless we're
        // currently dealing with a MoveArgs
        else if ( code == KeyEvent.VK_NUMPAD5 && instance != null && argState != moveArgs )
        {
            view.center( player.getInstLocation( instance ).getPoint() );
        }
        
        // all other requests are forwarded to the argState if it exists
        else if ( command != null )
        {
            // set the argState if necessary
            if ( argState == null )
                command.accept( this );
            else
                argState.keyPressed( e );
        }
        
        // on a key press, we update the view in case anything has changed
        updateView();
    }
    
    /**
     * Called when the game ticks. We need to make sure that all selected things
     * still exist, and the status overview gets updated
     */
    public void onTick()
    {
        // if anything is found to be nonexistent, everything represented by
        // boxes to its right must be reset
        if ( group != null )
        {
            List< SelectableGroup > groups = Arrays.asList( player.getGroups() );
            if ( !groups.contains( group ) )
            {
                group = null;
                type = null;
                instance = null;
                command = null;
                argState = null;
            }
        }
        if ( type != null )
        {
            List< SelectableType > types = Arrays.asList( group.getTypes() );
            if ( !types.contains( type ) )
            {
                type = null;
                instance = null;
                command = null;
                argState = null;
            }
        }
        if ( instance != null )
        {
            List< SelectableInstance > instances = Arrays.asList( type.getInstances() );
            if ( !instances.contains( instance ) )
            {
                instance = null;
                command = null;
                argState = null;
            }
            if ( instance != null ) // always refresh on tick
                updateStatus();
        }
        if ( command != null )
        {
            List< SelectableCommand > commands = Arrays.asList( instance.getCommands() );
            if ( !commands.contains( command ) )
            {
                command = null;
                argState = null;
            }
        }
        if ( argState != null )
        {
            argState.onTick();
        }
        
        updateView();
    }
    
    /**
     * Clear selections when the game begins.
     */
    public void onGameBegin()
    {
        clearSelections();
    }
    
    /**
     * Clear selections when you lose the game (damnit...)
     */
    public void onGameLose()
    {
        clearSelections();
    }
    
    /**
     * Clear selections when you win the game!
     */
    public void onGameWin()
    {
        clearSelections();
    }
    
    /**
     * it's a move command!
     */
    public void visitMoveArgs( MoveArgs args )
    {
        argState = moveArgs;
        argState.reset();
        System.out.println( "MoveArgs detected." );
    }
    
    /**
     * it's an army command!
     */
    public void visitArmyArgs( ArmyArgs args )
    {
        argState = armyArgs;
        argState.reset();
        System.out.println( "ArmyArgs detected." );
    }
    
    /**
     * it's a direction command!
     */
    public void visitDirectionArgs( DirectionArgs args )
    {
        argState = dirArgs;
        argState.reset();
        System.out.println( "DirectionArgs detected." );
    }
    
    /**
     * it's a no-args command!
     */
    public void visitNoArgs( NoArgs args )
    {
        argState = noArgs;
        argState.reset();
        System.out.println( "NoArgs detected." );
    }
    
    /**
     * Abstract class used to process argument key inputs. Each subclass knows
     * how to process a specific type of command.
     */
    abstract private class ArgState extends KeyAdapter
    {
        abstract public void reset();
        
        abstract public void keyPressed( KeyEvent e );
        
        abstract public void onTick();
    }
    
    /**
     * For processing MoveArgs. numpads 1-9 (not 5) are saved away as
     * directions. numpad 5 centers the view if no saved directions, or confirms
     * the saved directions if they exist.
     * 
     * On game ticks, the points sent to the View to be displayed must be
     * recalculated, in case the instance has moved.
     */
    private ArgState moveArgs = new ArgState()
    {
        private List< Direction > saved;
        
        private Direction[] dirs =
        {
            Direction.SW, // numpad 1
            Direction.S, // numpad 2
            Direction.SE, // numpad 3
            Direction.W, // numpad 4
            null, // numpad 5
            Direction.E, // numpad 6
            Direction.NW, // numpad 7
            Direction.N, // numpad 8
            Direction.NE, // numpad 9
        };
        
        /**
         * Clears out any saved directions.
         */
        public void reset()
        {
            saved = new ArrayList< Direction >();
        }
        
        /**
         * Processes the input.
         */
        public void keyPressed( KeyEvent e )
        {
            int code = e.getKeyCode();
            if ( code >= KeyEvent.VK_NUMPAD1 && code <= KeyEvent.VK_NUMPAD9 )
            {
                Direction d = dirs[ code - KeyEvent.VK_NUMPAD1 ];
                if ( d == null )
                {
                    // it was 5; center or send!
                    if ( saved.size() == 0 )
                        view.center( player.getInstLocation( instance ).getPoint() );
                    else
                    {
                        System.out.println( "executing moveargs command!" );
                        ((MoveArgs) command).setArgs( saved.toArray( new Direction[ 0 ] ) );
                        instance.addCommandToQueue( command );
                        view.setSelectedPath( new Point[ 0 ] );
                        command = null;
                        argState = null;
                    }
                }
                else
                {
                    System.out.println( "saving movement!" );
                    saved.add( d );
                    Point instPoint = player.getInstLocation( instance ).getPoint();
                    view.setSelectedPath( transform( saved, instPoint ) );
                }
            }
        }
        
        /**
         * Recalculate the selected path, send to View.
         */
        public void onTick()
        {
            Point instPoint = player.getInstLocation( instance ).getPoint();
            view.setSelectedPath( transform( saved, instPoint ) );
        }
    };
    
    /**
     * Transforms directions + a Point into a Point[]
     */
    private Point[] transform( List< Direction > dirs, Point offset )
    {
        Point[] points = new Point[ dirs.size() ];
        int offX = offset.x;
        int offY = offset.y;
        for ( int i = 0; i != points.length; ++i )
        {
            Direction d = dirs.get( i );
            points[ i ] = new Point( offX + d.x, offY + d.y );
            offX = offX + d.x;
            offY = offY + d.y;
        }
        
        return points;
    }
    
    /**
     * For processing ArmyArgs.
     * 
     * Waits for a 1-0 number that corresponds to a valid rally point ID.
     */
    private ArgState armyArgs = new ArgState()
    {
        /**
         * No state to reset.
         */
        public void reset()
        {
            // no state to reset
        }
        
        /**
         * Processes the input
         */
        public void keyPressed( KeyEvent e )
        {
            System.out.println( "key pressed..." );
            int code = e.getKeyCode();
            int ind = -1;
            if ( code >= KeyEvent.VK_1 && code <= KeyEvent.VK_9 )
            {
                ind = code - KeyEvent.VK_0;
            }
            else if ( code == KeyEvent.VK_0 && e.isControlDown() )
            {
                ind = 10;
            }
            if ( ind != -1 )
            {
                System.out.println( "attempting to join RP " + ind );
                SelectableRallyPoint[] rps = player.getRallyPoints();
                for ( SelectableRallyPoint r : rps )
                {
                    System.out.println( "looking at RP " + r.getID() );
                    if ( r.getID() == ind )
                    {
                        System.out.println( ind );
                        System.out.println( "executing armyargs command!" );
                        ((ArmyArgs) command).setArgs( ind );
                        instance.addCommandToQueue( command );
                        command = null;
                        argState = null;
                    }
                }
            }
        }
        
        /**
         * Nothing to be done on tick.
         */
        public void onTick()
        {
            // unneeded
        }
    };
    
    /**
     * For processing DirectionArgs.
     * 
     * numpad 1-9 (excluding 5) will be used for determining the direction.
     */
    private ArgState dirArgs = new ArgState()
    {
        private Direction[] dirs =
        {
            Direction.SW, // numpad 1
            Direction.S, // numpad 2
            Direction.SE, // numpad 3
            Direction.W, // numpad 4
            null, // numpad 5
            Direction.E, // numpad 6
            Direction.NW, // numpad 7
            Direction.N, // numpad 8
            Direction.NE, // numpad 9
        };
        
        /**
         * No state to reset.
         */
        public void reset()
        {
            // no state to reset
        }
        
        /**
         * Processes the input.
         */
        public void keyPressed( KeyEvent e )
        {
            int code = e.getKeyCode();
            if ( code >= KeyEvent.VK_NUMPAD1 && code <= KeyEvent.VK_NUMPAD9 )
            {
                Direction d = dirs[ code - KeyEvent.VK_NUMPAD1 ];
                if ( d != null )
                {
                    System.out.println( "executing dirargs command!" );
                    ((DirectionArgs) command).setArgs( d );
                    instance.addCommandToQueue( command );
                    command = null;
                    argState = null;
                }
            }
        }
        
        /**
         * Nothing to do on tick.
         */
        public void onTick()
        {
            // unneeded
        }
    };
    
    /**
     * For processing NoArgs.
     * 
     * The command is added to the queue when ENTER is pressed.
     */
    private ArgState noArgs = new ArgState()
    {
        /**
         * No state to reset.
         */
        public void reset()
        {
            // no state to reset
        }
        
        /**
         * Processes the input.
         */
        public void keyPressed( KeyEvent e )
        {
            if ( e.getKeyCode() == KeyEvent.VK_ENTER )
            {
                System.out.println( "executing noargs command!" );
                instance.addCommandToQueue( command );
                command = null;
                argState = null;
            }
        }
        
        /**
         * Nothing to do on tick.
         */
        public void onTick()
        {
            // unneeded
        }
    };
}
